Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 1507389b180c5671bc5eca948930b39f22f8b36d


Parents : afdf987
Author : Mark Qvist <mark@unsigned.io>
Date : 2025-11-23T23:55:01+01:00

C it is, then

Changes

3 files changed, 210 insertions(+), 60 deletions(-)

M LXST/Filters.py +133 -60

Diff

diff --git a/LXST/Filters.c b/LXST/Filters.c
new file mode 100644
index 0000000..d0fabab
--- /dev/null
+++ b/LXST/Filters.c
@@ -0,0 +1,74 @@
+#include <math.h>
+
+void highpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states, float* last_inputs) {
+ int i, ch;
+ for (ch = 0; ch < channels; ch++) { float input_diff = input[ch] - last_inputs[ch]; output[ch] = alpha * (filter_states[ch] + input_diff); }
+ for (i = 1; i < samples; i++) { for (ch = 0; ch < channels; ch++) { int idx = i * channels + ch; float input_diff = input[idx] - input[idx - channels]; output[idx] = alpha * (output[idx - channels] + input_diff); } }
+ for (ch = 0; ch < channels; ch++) { int last_idx = (samples - 1) * channels + ch; filter_states[ch] = output[last_idx]; last_inputs[ch] = input[last_idx]; }
+}
+
+void lowpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states) {
+ int i, ch;
+ float one_minus_alpha = 1.0f - alpha;
+ for (ch = 0; ch < channels; ch++) { output[ch] = alpha * input[ch] + one_minus_alpha * filter_states[ch]; }
+ for (i = 1; i < samples; i++) {
+ for (ch = 0; ch < channels; ch++) { int idx = i * channels + ch; output[idx] = alpha * input[idx] + one_minus_alpha * output[idx - channels]; }
+ }
+ for (ch = 0; ch < channels; ch++) { int last_idx = (samples - 1) * channels + ch; filter_states[ch] = output[last_idx]; }
+}
+
+void agc_process(float* input, float* output, int samples, int channels, float target_linear, float max_gain_linear, float trigger_level,
+ float attack_coeff, float release_coeff, float hold_samples, float* current_gain_lin, int* hold_counter, int block_target) {
+
+ for (int i = 0; i < samples * channels; i++) { output[i] = input[i]; }
+ int num_blocks = block_target;
+ int block_size = samples / num_blocks;
+ if (block_size < 1) block_size = 1;
+
+ for (int block = 0; block < num_blocks; block++) {
+ int block_start = block * block_size;
+ int block_end = (block + 1) * block_size;
+ if (block == num_blocks - 1) block_end = samples;
+ if (block_end > samples) block_end = samples;
+
+ int block_samples = block_end - block_start;
+ if (block_samples <= 0) continue;
+
+ for (int ch = 0; ch < channels; ch++) {
+ float sum_squares = 0.0f;
+ for (int i = block_start; i < block_end; i++) { int idx = i * channels + ch; sum_squares += output[idx] * output[idx]; }
+ float rms = sqrtf(sum_squares / block_samples);
+
+ float target_gain;
+ if (rms > 1e-9f && rms > trigger_level) {
+ target_gain = target_linear / rms;
+ if (target_gain > max_gain_linear) { target_gain = max_gain_linear; }
+ } else { target_gain = current_gain_lin[ch]; }
+
+ if (target_gain < current_gain_lin[ch]) {
+ current_gain_lin[ch] = attack_coeff * target_gain + (1.0f - attack_coeff) * current_gain_lin[ch];
+ *hold_counter = (int)hold_samples;
+ } else {
+ if (*hold_counter > 0) { *hold_counter -= block_samples; }
+ else { current_gain_lin[ch] = release_coeff * target_gain + (1.0f - release_coeff) * current_gain_lin[ch]; }
+ }
+
+ for (int i = block_start; i < block_end; i++) { int idx = i * channels + ch; output[idx] *= current_gain_lin[ch]; }
+ }
+ }
+
+ float peak_limit = 0.75f;
+ for (int ch = 0; ch < channels; ch++) {
+ float peak = 0.0f;
+ for (int i = 0; i < samples; i++) {
+ int idx = i * channels + ch;
+ float abs_val = fabsf(output[idx]);
+ if (abs_val > peak) peak = abs_val;
+ }
+
+ if (peak > peak_limit) {
+ float scale = peak_limit / peak;
+ for (int i = 0; i < samples; i++) { int idx = i * channels + ch; output[idx] *= scale; }
+ }
+ }
+}
\ No newline at end of file

diff --git a/LXST/Filters.h b/LXST/Filters.h
new file mode 100644
index 0000000..cefeb6c
--- /dev/null
+++ b/LXST/Filters.h
@@ -0,0 +1,3 @@
+void highpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states, float* last_inputs);
+void lowpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states);
+void agc_process(float* input, float* output, int samples, int channels, float target_linear, float max_gain_linear, float trigger_level, float attack_coeff, float release_coeff, float hold_samples, float* current_gain_lin, int* hold_counter, int block_target);
\ No newline at end of file

diff --git a/LXST/Filters.py b/LXST/Filters.py
index c924af8..4124361 100644
--- a/LXST/Filters.py
+++ b/LXST/Filters.py
@@ -1,6 +1,28 @@
+from importlib.util import find_spec
import numpy as np
import time
import RNS
+import os
+
+if not find_spec("cffi"):
+ USE_NATIVE_FILTERS = False
+ RNS.log(f"Could not load CFFI module for filter acceleration, falling back to Python filters. This will be slow.", RNS.LOG_WARNING)
+ RNS.log(f"Make sure that the CFFI module is installed and available.", RNS.LOG_WARNING)
+else:
+ try:
+ # TODO: Load pre-compiled so/dll
+ from cffi import FFI
+ import pathlib
+ ffi = FFI()
+ c_src_path = pathlib.Path(__file__).parent.resolve()
+ with open(os.path.join(c_src_path, "Filters.h"), "r") as f: ffi.cdef(f.read())
+ with open(os.path.join(c_src_path, "Filters.c"), "r") as f: c_src = f.read()
+ native_functions = ffi.verify(c_src)
+ USE_NATIVE_FILTERS = True
+ except Exception as e:
+ RNS.log(f"Could not compile modules for filter acceleration, falling back to Python filters. This will be slow.", RNS.LOG_WARNING)
+ RNS.log(f"The contained exception was: {e}", RNS.LOG_WARNING)
+ USE_NATIVE_FILTERS = False
class Filter():
def handle_frame(self, frame):
@@ -17,7 +39,6 @@ class HighPass(Filter):
self._alpha = None
def handle_frame(self, frame, samplerate):
- st = time.time()
if len(frame) == 0: return frame
if samplerate != self._samplerate:
self._samplerate = samplerate
@@ -34,24 +55,38 @@ class HighPass(Filter):
self._filter_states = np.zeros(self._channels, dtype=np.float32)
self._last_inputs = np.zeros(self._channels, dtype=np.float32)
- output = np.empty_like(frame_2d)
- input_diff_first = frame_2d[0] - self._last_inputs
- output[0] = self._alpha * (self._filter_states + input_diff_first)
-
- input_diff = np.empty_like(frame_2d)
- input_diff[0] = input_diff_first
- input_diff[1:] = frame_2d[1:] - frame_2d[:-1]
+ if USE_NATIVE_FILTERS:
+ frame_2d = np.ascontiguousarray(frame_2d, dtype=np.float32)
+ output = np.empty_like(frame_2d, dtype=np.float32)
+ input_ptr = ffi.cast("float *", frame_2d.ctypes.data)
+ output_ptr = ffi.cast("float *", output.ctypes.data)
+ states_ptr = ffi.cast("float *", self._filter_states.ctypes.data)
+ last_inputs_ptr = ffi.cast("float *", self._last_inputs.ctypes.data)
+
+ native_functions.highpass_filter(input_ptr, output_ptr, samples, channels, float(self._alpha), states_ptr, last_inputs_ptr)
+
+ result = output.reshape(frame.shape)
+ return result
- print(f"Filtered in {RNS.prettyshorttime(time.time()-st)}")
-
-
- for i in range(1, samples): output[i] = self._alpha * (output[i-1] + input_diff[i])
-
- self._filter_states = output[-1].copy()
- self._last_inputs = frame_2d[-1].copy()
-
- nframe = output.reshape(frame.shape)
- return nframe
+ else:
+ output = np.empty_like(frame_2d)
+ input_diff_first = frame_2d[0] - self._last_inputs
+ output[0] = self._alpha * (self._filter_states + input_diff_first)
+
+ input_diff = np.empty_like(frame_2d)
+ input_diff[0] = input_diff_first
+ input_diff[1:] = frame_2d[1:] - frame_2d[:-1]
+
+ for i in range(1, samples):
+ output[i] = self._alpha * (output[i-1] + input_diff[i])
+
+ output = self._alpha * (output + input_diff)
+
+ self._filter_states = output[-1].copy()
+ self._last_inputs = frame_2d[-1].copy()
+
+ nframe = output.reshape(frame.shape)
+ return nframe
class LowPass(Filter):
def __init__(self, cut):
@@ -78,14 +113,27 @@ class LowPass(Filter):
if self._filter_states is None or self._channels != channels:
self._channels = channels
self._filter_states = np.zeros(self._channels, dtype=np.float32)
-
- output = np.empty_like(frame_2d)
- output[0] = self._alpha * frame_2d[0] + (1.0 - self._alpha) * self._filter_states
- for i in range(1, samples): output[i] = self._alpha * frame_2d[i] + (1.0 - self._alpha) * output[i-1]
-
- self._filter_states = output[-1].copy()
-
- return output.reshape(frame.shape)
+
+ if USE_NATIVE_FILTERS:
+ frame_2d = np.ascontiguousarray(frame_2d, dtype=np.float32)
+ output = np.empty_like(frame_2d, dtype=np.float32)
+ input_ptr = ffi.cast("float *", frame_2d.ctypes.data)
+ output_ptr = ffi.cast("float *", output.ctypes.data)
+ states_ptr = ffi.cast("float *", self._filter_states.ctypes.data)
+
+ native_functions.lowpass_filter(input_ptr, output_ptr, samples, channels, float(self._alpha), states_ptr)
+
+ return output.reshape(frame.shape)
+
+ else:
+ output = np.empty_like(frame_2d)
+ output[0] = self._alpha * frame_2d[0] + (1.0 - self._alpha) * self._filter_states
+ for i in range(1, samples):
+ output[i] = self._alpha * frame_2d[i] + (1.0 - self._alpha) * output[i-1]
+
+ self._filter_states = output[-1].copy()
+
+ return output.reshape(frame.shape)
class BandPass(Filter):
def __init__(self, low_cut, high_cut):
@@ -97,9 +145,12 @@ class BandPass(Filter):
self._low_pass = LowPass(self.high_cut)
def handle_frame(self, frame, samplerate):
+ # TODO: Remove debug
+ # st = time.time()
if len(frame) == 0: return frame
high_passed = self._high_pass.handle_frame(frame, samplerate)
band_passed = self._low_pass.handle_frame(high_passed, samplerate)
+ # RNS.log(f"Filter ran in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG)
return band_passed
class AGC(Filter):
@@ -123,7 +174,8 @@ class AGC(Filter):
self._hold_samples = None
def handle_frame(self, frame, samplerate):
- st = time.time()
+ # TODO: Remove debug
+ # st = time.time()
if len(frame) == 0: return frame
if len(frame.shape) == 1: frame_2d = frame.reshape(-1, 1)
else: frame_2d = frame
@@ -138,45 +190,66 @@ class AGC(Filter):
self._channels = channels
self._current_gain_lin = np.ones(channels, dtype=np.float32)
self._hold_counter = 0
-
- output = np.empty_like(frame_2d)
- block_size = max(1, samples // self._block_target)
- for i in range(0, samples, block_size):
- block_end = min(i + block_size, samples)
- block = frame_2d[i:block_end]
- block_samples = block_end - i
+
+ if USE_NATIVE_FILTERS:
+ frame_2d = np.ascontiguousarray(frame_2d, dtype=np.float32)
+ output = np.empty_like(frame_2d, dtype=np.float32)
+ input_ptr = ffi.cast("float *", frame_2d.ctypes.data)
+ output_ptr = ffi.cast("float *", output.ctypes.data)
+ gain_ptr = ffi.cast("float *", self._current_gain_lin.ctypes.data)
+ hold_ptr = ffi.new("int *", self._hold_counter)
- rms = np.sqrt(np.mean(block ** 2, axis=0))
- target_gain = np.where(rms > 1e-9, self.target_linear / np.maximum(rms, 1e-9), self.max_gain_linear)
- target_gain = np.minimum(target_gain, self.max_gain_linear)
- smoothed_gain = np.empty_like(target_gain)
+ native_functions.agc_process(
+ input_ptr, output_ptr, samples, channels,
+ float(self.target_linear), float(self.max_gain_linear),
+ float(self.trigger_level),
+ float(self._attack_coeff), float(self._release_coeff),
+ float(self._hold_samples),
+ gain_ptr, hold_ptr, int(self._block_target)
+ )
- for ch in range(channels):
- if (rms[0] < self.trigger_level): target_gain = self._current_gain_lin
- if target_gain[ch] < self._current_gain_lin[ch]:
- self._current_gain_lin[ch] = self._attack_coeff * target_gain[ch] + (1 - self._attack_coeff) * self._current_gain_lin[ch]
- self._hold_counter = self._hold_samples # Reset hold counter
- else:
- if self._hold_counter > 0: self._hold_counter -= block_samples
- else: self._current_gain_lin[ch] = self._release_coeff * target_gain[ch] + (1 - self._release_coeff) * self._current_gain_lin[ch]
-
- smoothed_gain[ch] = self._current_gain_lin[ch]
+ self._hold_counter = hold_ptr[0]
+ result = output.reshape(frame.shape)
# TODO: Remove debug
- # if (rms[0] < self.trigger_level): print(f"Ambient RMS={round(rms[0], 4)} ", end="")
- # else: print(f"Voice RMS={round(rms[0], 4)} ", end="")
- # print(f"smoothed_gain={round(smoothed_gain[0], 2)}, tg={round(target_gain[0], 4)}")
+ # RNS.log(f"AGC ran in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG)
+ return result
- output[i:block_end] = block * smoothed_gain[np.newaxis, :]
-
- peak_limit = 0.75
- current_peaks = np.max(np.abs(output), axis=0)
- limit_gain = np.where(current_peaks > peak_limit, peak_limit / np.maximum(current_peaks, 1e-9), 1.0)
+ else:
+ output = np.empty_like(frame_2d)
+ block_size = max(1, samples // self._block_target)
+ for i in range(0, samples, block_size):
+ block_end = min(i + block_size, samples)
+ block = frame_2d[i:block_end]
+ block_samples = block_end - i
+
+ rms = np.sqrt(np.mean(block ** 2, axis=0))
+ target_gain = np.where(rms > 1e-9, self.target_linear / np.maximum(rms, 1e-9), self.max_gain_linear)
+ target_gain = np.minimum(target_gain, self.max_gain_linear)
+ smoothed_gain = np.empty_like(target_gain)
+
+ for ch in range(channels):
+ if (rms[0] < self.trigger_level): target_gain = self._current_gain_lin
+ if target_gain[ch] < self._current_gain_lin[ch]:
+ self._current_gain_lin[ch] = self._attack_coeff * target_gain[ch] + (1 - self._attack_coeff) * self._current_gain_lin[ch]
+ self._hold_counter = self._hold_samples # Reset hold counter
+ else:
+ if self._hold_counter > 0: self._hold_counter -= block_samples
+ else: self._current_gain_lin[ch] = self._release_coeff * target_gain[ch] + (1 - self._release_coeff) * self._current_gain_lin[ch]
+
+ smoothed_gain[ch] = self._current_gain_lin[ch]
+
+ output[i:block_end] = block * smoothed_gain[np.newaxis, :]
+
+ peak_limit = 0.75
+ current_peaks = np.max(np.abs(output), axis=0)
+ limit_gain = np.where(current_peaks > peak_limit, peak_limit / np.maximum(current_peaks, 1e-9), 1.0)
- if np.any(limit_gain < 1.0): output *= limit_gain[np.newaxis, :]
- nframe = output.reshape(frame.shape)
- print(f"AGC in {RNS.prettyshorttime(time.time()-st)}")
- return nframe
+ if np.any(limit_gain < 1.0): output *= limit_gain[np.newaxis, :]
+ nframe = output.reshape(frame.shape)
+ # TODO: Remove debug
+ # RNS.log(f"AGC ran in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG)
+ return nframe
def _calculate_coefficients(self):
if self._samplerate:


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────